﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Encapsulates the boot block.
    /// </summary>
    public class BootBlock : Object, IFsComponent
    {
        #region Properties

        /// <summary>
        /// Contains the boot sector.
        /// </summary>
        private readonly byte[] FSector;

        #region Modifiable

        /// <summary>
        /// Gets or sets the time stamp.
        /// </summary>
        public TimeStamp TimeStamp
        {
            [DebuggerStepThrough]
            get
            {
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x194;
                    return new TimeStamp(br.ReadInt64());
                }
            }
            [DebuggerStepThrough]
            set
            {
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x194;
                    bw.Write(value.ToInt64());
                }
            }
        }

        /// <summary>
        /// Gets or sets the size of the data area in bytes.
        /// </summary>
        /// <exception cref="ArgumentException">An attempt is made to set the data area size to a value that is not the multiple of the block size.</exception>
        public Int64 DataAreaSize
        {
            [DebuggerStepThrough]
            get
            {
                int blockSize = (128 << FSector[0x1BC]);
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x19C;
                    long value = br.ReadInt64();
                    return value * blockSize;
                }
            }
            [DebuggerStepThrough]
            set
            {
                int blockSize = (128 << FSector[0x1BC]);
                if ((value % blockSize) != 0)
                    throw new DataAreaSizeNotMultipleOfBlockSizeException("value");
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x19C;
                    bw.Write(value / blockSize);
                }
            }
        }

        /// <summary>
        /// Gets or sets the size of the index area in bytes.
        /// </summary>
        /// <exception cref="ArgumentException">An attempt is made to set the index area size to a value that is not the multiple of the block size.</exception>
        public Int64 IndexAreaSize
        {
            [DebuggerStepThrough]
            get
            {
                //int blockSize = (int)(128 << FSector[0x1BC]);
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1A4;
                    long value = br.ReadInt64();
                    //return value * blockSize;
                    // (bug) index area size is in BYTES, not BLOCKS.
                    return value;
                }
            }
            [DebuggerStepThrough]
            set
            {
                //int blockSize = (int)(128 << FSector[0x1BC]);
                //if ((value % blockSize) != 0)
                if ((value % 64) != 0)
                    throw new IndexAreaSizeNotMultipleOf64Exception("value");
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1A4;
                    //bw.Write(value / blockSize);
                    // (bug) index area size is in BYTES, not BLOCKS.
                    bw.Write(value);
                }
            }
        }

        /// <summary>
        /// Gets or sets the magic number.
        /// </summary>
        /// <remarks>The magic number is <em>0x534653</em>.</remarks>
        public Int32 MagicNumber
        {
            [DebuggerStepThrough]
            get
            {
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1AC;
                    int value = br.ReadInt32();
                    return value & 0x00FFFFFF;
                }
            }
            [DebuggerStepThrough]
            set
            {
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1AC;
                    byte version = FSector[0x1AF];
                    bw.Write((value & 0x00FFFFFF) | (version << 24));
                }
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets or sets the magic number.
        /// </summary>
        /// <remarks>The magic number is <em>0x10</em>.</remarks>
        public Byte VersionNumber
        {
            [DebuggerStepThrough]
            get
            {
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1AF;
                    return br.ReadByte();
                }
            }
            [DebuggerStepThrough]
            set
            {
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1AF;
                    bw.Write(value);
                }
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets or sets the magic number with version number.
        /// </summary>
        /// <remarks>The magic number with version number is <em>0x10534653</em>.</remarks>
        public Int32 MagicNumberWithVersionNumber
        {
            [DebuggerStepThrough]
            get
            {
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1AC;
                    return br.ReadInt32();
                }
            }
            [DebuggerStepThrough]
            set
            {
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1AC;
                    bw.Write(value);
                }
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets or sets the size of the complete volume in bytes.
        /// </summary>
        /// <exception cref="ArgumentException">An attempt is made to set the volume size to a value that is not the multiple of the block size.</exception>
        public Int64 VolumeSize
        {
            [DebuggerStepThrough]
            get
            {
                int blockSize = (128 << FSector[0x1BC]);
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1B0;
                    long value = br.ReadInt64();
                    return value * blockSize;
                }
            }
            [DebuggerStepThrough]
            set
            {
                int blockSize = (128 << FSector[0x1BC]);
                if ((value % blockSize) != 0)
                    throw new VolumeSizeNotMultipleOfBlockSizeException("value");
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1B0;
                    bw.Write(value / blockSize);
                }
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets or sets the size of the reserved area at the start of the volume in bytes.
        /// </summary>
        /// <exception cref="ArgumentException">An attempt is made to set the reserved area size to a value that is not the multiple of the block size.</exception>
        /// <exception cref="ArgumentOutOfRangeException">An attempt is made to set the reserved area size larger than <em>2^31</em> blocks.</exception>
        public Int64 ReservedAreaSize
        {
            [DebuggerStepThrough]
            get
            {
                int blockSize = (128 << FSector[0x1BC]);
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1B8;
                    long value = br.ReadInt32();
                    return value * blockSize;
                }
            }
            [DebuggerStepThrough]
            set
            {
                int blockSize = (128 << FSector[0x1BC]);
                if ((value % blockSize) != 0)
                    throw new ReservedAreaSizeNotMultipleOfBlockSizeException("value");
                if ((value / blockSize) > Int32.MaxValue)
                    throw new ReservedAreaSizeTooLargeException("value");
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1B8;
                    int size = (int)(value / blockSize);
                    bw.Write(size);
                }
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets or sets the block size in bytes.
        /// </summary>
        /// <exception cref="ArgumentException"></exception>
        public Int32 BlockSize
        {
            [DebuggerStepThrough]
            get
            {
                // Prevent overflows. FS open code will validate this value anyway.
                if (FSector[0x1BC] >= (31 - 7))
                    return Int32.MaxValue;
                int blockSize = (128 << FSector[0x1BC]);
                return blockSize;
            }
            [DebuggerStepThrough]
            set
            {
                int index;
                for (index = 0; index < 31; index++)
                {
                    if (value == (1 << index))
                        break;
                }
                if (index >= 31)
                    throw new BlockSizeNotPowerOfTwoException("value");
                if (index < 8)
                    throw new BlockSizeTooSmallPowerOfTwoException("value");
                if (index > 24)
                    throw new BlockSizeTooLargePowerOfTwoException("value");
                FSector[0x1BC] = (byte)((index - 7) & 0xFF);
                CalculateChecksum();
            }
        }

        /// <summary>
        /// Gets the checksum.
        /// </summary>
        public Byte Checksum
        {
            [DebuggerStepThrough]
            get
            {
                return FSector[0x1BD];
            }
        }

        /// <summary>
        /// Gets or sets the boot signature.
        /// </summary>
        /// <remarks>The boot signature is <em>0xAA55</em>.</remarks>
        public Int32 BootSignature
        {
            [DebuggerStepThrough]
            get
            {
                using (BinaryReader br = new BinaryReader(new MemoryStream(FSector, false)))
                {
                    br.BaseStream.Position = 0x1FE;
                    return br.ReadInt16();
                }
            }
            [DebuggerStepThrough]
            set
            {
                using (BinaryWriter bw = new BinaryWriter(new MemoryStream(FSector, true)))
                {
                    bw.BaseStream.Position = 0x1FE;
                    bw.Write((ushort)(value & 0xFFFF));
                }
            }
        }

        #endregion

        #region Computed

        /// <summary>
        /// Gets the value indicating whether the <see cref="Checksum"/> is valid.
        /// </summary>
        public Boolean ChecksumValid
        {
            [DebuggerStepThrough]
            get
            {
                byte checksum = 0;
                for (int i = 0x1AC; i < 0x1BD; i++)
                {
                    checksum += FSector[i];
                }
                // Now negate and plug in the correct checksume.
                return (FSector[0x1BD] == (byte)((256 - checksum) & 0xFF));
            }
        }

        /// <summary>
        /// Gets the zero-based block number of the first block that is used by the index area.
        /// </summary>
        /// <value>The zero-based block number of the first block in the index area.</value>
        public Int64 FirstIndexAreaBlock
        {
            [DebuggerStepThrough]
            get
            {
                return (VolumeSize - IndexAreaSize) / BlockSize;
            }
        }

        /// <summary>
        /// Gets the zero-based block number of the first block <em>after</em> the data area.
        /// </summary>
        /// <value>The zero-based block number of the first block after the data area.</value>
        public Int64 FirstFreeAreaBlock
        {
            [DebuggerStepThrough]
            get
            {
                return (ReservedAreaSize + DataAreaSize) / BlockSize;
            }
        }

        /// <summary>
        /// Gets the zero-based block number of the first block that is used by the data area.
        /// </summary>
        /// <value>The zero-based block number of the first block in the data area.</value>
        public Int64 FirstDataAreaBlock
        {
            [DebuggerStepThrough]
            get
            {
                return (ReservedAreaSize) / BlockSize;
            }
        }

        /// <summary>
        /// Gets the total count of blocks in the volume.
        /// </summary>
        /// <value>The count of blocks in the volume.</value>
        public Int64 BlockCountInVolume
        {
            [DebuggerStepThrough]
            get
            {
                return (VolumeSize) / BlockSize;
            }
        }

        /// <summary>
        /// Gets the count of blocks in the index area.
        /// </summary>
        /// <value>The count of blocks in the index area.</value>
        public Int64 IndexAreaBlockCount
        {
            [DebuggerStepThrough]
            get
            {
                return (IndexAreaSize + BlockSize - 1) / BlockSize;
            }
        }

        #endregion

        #endregion

        #region Methods

        /// <summary>
        /// Gets a <see cref="Stream"/> that can be used to access the first part of the boot code contained in the boot block.
        /// </summary>
        /// <returns></returns>
        [DebuggerStepThrough]
        public Stream GetBootCode1()
        {
            return new MemoryStream(FSector, 0x0, 11, true);
        }

        /// <summary>
        /// Gets a <see cref="Stream"/> that can be used to access the BIOS Parameter Block contained in the boot block.
        /// </summary>
        /// <returns></returns>
        [DebuggerStepThrough]
        public Stream GetBiosParameterBlock()
        {
            return new MemoryStream(FSector, 0x0B, 21, true);
        }

        /// <summary>
        /// Gets a <see cref="Stream"/> that can be used to access the second part of the boot code contained in the boot block.
        /// </summary>
        /// <returns></returns>
        [DebuggerStepThrough]
        public Stream GetBootCode2()
        {
            return new MemoryStream(FSector, 0x20, 372, true);
        }

        /// <summary>
        /// Gets a <see cref="Stream"/> that can be used to access the partition table contained in the boot block.
        /// </summary>
        /// <returns></returns>
        [DebuggerStepThrough]
        public Stream GetPartitionTable()
        {
            return new MemoryStream(FSector, 0x1BE, 64, true);
        }

        /// <summary>
        /// Updates the checksum.
        /// </summary>
        [DebuggerStepThrough]
        private void CalculateChecksum()
        {
            byte checksum = 0;
            for (int i = 0x1AC; i < 0x1BD; i++)
            {
                checksum += FSector[i];
            }
            // Now negate and plug in the correct checksume.
            FSector[0x1BD] = (byte)((256 - checksum) & 0xFF);
        }

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="BootBlock"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="BootBlock"/>.
        /// </summary>
        [DebuggerStepThrough]
        public BootBlock()
            : base()
        {
            FSector = new byte[512];
            // Fill with dummy values.
            FSector[0x1BC] = 2; // 512 bytes per block.
            MagicNumberWithVersionNumber = 0x10534653;
            CalculateChecksum();
            VolumeSize = 512;
            ReservedAreaSize = 512;
        }
        /// <summary>
        /// Initializes a new instance of <see cref="BootBlock"/>.
        /// </summary>
        /// <param name="volume">The volume to read the boot block from.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="volume"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public BootBlock(IVolume volume)
        {
            if (volume == null)
                throw new ArgumentNullException("volume");
            FSector = volume.Read(0, 512);
            Debug.Assert(FSector != null && FSector.Length == 512);
        }
        #endregion

        #region IFsComponent Members

        /// <summary>
        /// Writes the component back to the volume.
        /// </summary>
        /// <param name="volume">The volume.</param>
        [DebuggerStepThrough]
        void IFsComponent.WriteTo(IVolume volume)
        {
            volume.Write(0, FSector);
        }

        #endregion
    }
}
